/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or https://opensource.org/licenses/CDDL-1.0.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright (c) 2022 Argo Technologie East
 */

#include <assert.h>
#include <errno.h>
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <thread.h>
#include <unistd.h>

#include <libzfs.h>
#include <libzfs_core.h>

typedef struct get_all_state {
	get_all_cb_t	*ga_cbp;
	char 		*ga_prefix_path;
} get_all_state_t;

typedef struct options {
	boolean_t	op_verbose;
	boolean_t	op_continue;
	int		op_seconds;
	boolean_t	op_result;
	char 		*op_prefix_path;
} options_t;

typedef struct mount_state {
	options_t	*sm_options;
	mutex_t		sm_lock; /* protects the remaining fields */
} mount_state_t;

typedef struct mounted_list {
	int		ml_count;
	int		ml_capacity;
	char		**ml_mountpoints;
} mounted_list_t;

static mounted_list_t g_ml;

static void 
extend_mountedlist()
{
	assert(g_ml.ml_count == g_ml.ml_capacity);

	int capacity = g_ml.ml_capacity == 0 ? 20 : g_ml.ml_capacity * 2;
	char **mountpoints = realloc(g_ml.ml_mountpoints,
	    capacity * sizeof (char *));

	assert(mountpoints != NULL);

	g_ml.ml_capacity = capacity;
	g_ml.ml_mountpoints = mountpoints;
}

static void
init_mountedlist()
{
	g_ml.ml_count = 0;
	g_ml.ml_capacity = 0;
	g_ml.ml_mountpoints = NULL;
}

static char *
check_mountedlist(char *mp)
{
	int len = strlen(mp);

	/*
	 * Check: find children mount point
	 */
	for (int i = 0; i < g_ml.ml_count; i++) {
		char *pmp = g_ml.ml_mountpoints[i];

		/*
		 * Children mount point must be longer
		 * and have '/' after the end of mp
		 */
		if (len >= strlen(pmp))
			continue;

		if (strncmp(mp, pmp, len) != 0)
			continue;
		
		if (pmp[len] != '/')
			continue;
		
		return (pmp);
	}

	return (NULL);
}

static void
add_into_mountedlist(char *mp)
{
	if (g_ml.ml_count == g_ml.ml_capacity)
		extend_mountedlist();

	g_ml.ml_mountpoints[g_ml.ml_count++] = mp;
}

static void
fini_mountedlist()
{
	for (int i = 0; i < g_ml.ml_count; i++)
		free(g_ml.ml_mountpoints[i]);
	
	free(g_ml.ml_mountpoints);
}

static char *
get_mount_point(zfs_handle_t *zhp)
{
	char mounta[MAXPATHLEN] = {'\0'};

	if (zfs_get_type(zhp) != ZFS_TYPE_FILESYSTEM)
		return (NULL);

	verify(zfs_prop_get(zhp, ZFS_PROP_MOUNTPOINT, mounta,
	    sizeof (mounta), NULL, NULL, 0, B_FALSE) == 0);

	return (strdup(mounta));
}

static int
get_one_dataset(zfs_handle_t *zhp, void *data)
{
	get_all_state_t *state = data;
	zfs_type_t type = zfs_get_type(zhp);

	/*
	 * Iterate over any nested datasets.
	 */
	if (zfs_iter_filesystems_v2(zhp, 0, get_one_dataset, data) != 0) {
		zfs_close(zhp);
		return (1);
	}

	/*
	 * Skip any datasets whose type does not match.
	 */
	if ((type & ZFS_TYPE_FILESYSTEM) == 0) {
		zfs_close(zhp);
		return (0);
	}

	/*
	 * Skip any datasets whose path does not match the prefix.
	 */
	if (state->ga_prefix_path != NULL) {
		char *mp = get_mount_point(zhp);
		int len = strlen(state->ga_prefix_path);
		boolean_t skip = strncmp(state->ga_prefix_path, mp, len) != 0;
		free(mp);

		if (skip) {
			zfs_close(zhp);
			return (0);
		}
	}

	libzfs_add_handle(state->ga_cbp, zhp);
	assert(state->ga_cbp->cb_used <= state->ga_cbp->cb_alloc);

	return (0);
}

static void
get_all_datasets(libzfs_handle_t *g_zfs, get_all_cb_t *cbp, char *prefix_path)
{
	get_all_state_t state = {
	    .ga_cbp = cbp,
	    .ga_prefix_path = prefix_path
	};

	(void) zfs_iter_root(g_zfs, get_one_dataset, &state);
}

static boolean_t
is_legacy_mount_point(const char *mount_point)
{
	if (mount_point == NULL)
		return (B_FALSE);

	if (*mount_point == '\0')
		return (B_FALSE);

	if (*mount_point != '/')
		return (B_FALSE);
	
	return (B_TRUE);
}

static boolean_t
emulation_mount_one(zfs_handle_t *zhp, mount_state_t *sms)
{
	boolean_t res = B_TRUE;
	char *mp = get_mount_point(zhp);
	boolean_t verbose = sms->sm_options->op_verbose;
	int seconds = sms->sm_options->op_seconds;

	if (!is_legacy_mount_point(mp)) {
		free(mp);
		return (res);
	}

	if (verbose)
		printf("zfs mount -o mount_point %s %s\n",
		    mp, zfs_get_name(zhp));

	mutex_enter(&sms->sm_lock);	/* array is thread unsafe */
	char *parent_mp = check_mountedlist(mp);
	mutex_exit(&sms->sm_lock);

	if (parent_mp != NULL) {
		res = B_FALSE;
		if (verbose)
			printf("Mount point %s is mounting before parent %s\n",
			    mp, parent_mp);
	}

	if (seconds > 0)
		sleep(seconds);

	mutex_enter(&sms->sm_lock);
	add_into_mountedlist(mp);	/* after this do not free mp */
	mutex_exit(&sms->sm_lock);

	return (res);
}

static int
mount_one_cb(zfs_handle_t *zhp, void *arg)
{
	mount_state_t *sms = arg;

	if (!emulation_mount_one(zhp, sms)) {
		mutex_enter(&sms->sm_lock);
		sms->sm_options->op_result = B_FALSE;
		mutex_exit(&sms->sm_lock);

		if (!sms->sm_options->op_continue)
			return (1);
	}

	return (0);	/* continue to check */
}

static void
dump_handles(const char *when, const get_all_cb_t *cb)
{
	size_t count = cb->cb_used;
	zfs_handle_t **zhpp = cb->cb_handles;

	printf("--------------------------\n");
	printf("dump zfs_handles (%s), record count: %d\n", when, count);
	for (size_t i = 0; i < count; ++i) {
		zfs_handle_t *zhp = zhpp[i];
		char *mount_point = get_mount_point(zhp);

		printf("[%d] mount point: %s, volume: %s\n", i,
		    mount_point, zfs_get_name(zhp));
		free(mount_point);
	}
	printf("--------------------------\n");
}

static boolean_t
do_mount_all(libzfs_handle_t *g_zfs, options_t *opts)
{
	get_all_cb_t cb = { 0 };

	get_all_datasets(g_zfs, &cb, opts->op_prefix_path);

	if (opts->op_verbose)
		dump_handles("original", &cb);

	mount_state_t state = { 0 };
	state.sm_options = opts;
	mutex_init(&state.sm_lock, LOCK_NORMAL | LOCK_ERRORCHECK, NULL);
	init_mountedlist();

	zfs_foreach_mountpoint(g_zfs, cb.cb_handles, cb.cb_used,
	    mount_one_cb, &state, B_TRUE);

	boolean_t res = state.sm_options->op_result;
	fini_mountedlist();
	mutex_destroy(&state.sm_lock);

	if (opts->op_verbose)
		dump_handles("after sort", &cb);

	for (int i = 0; i < cb.cb_used; i++)
		zfs_close(cb.cb_handles[i]);
	free(cb.cb_handles);

	return (res);
}

static void
usage(char *prog)
{
	printf("Usage: %s [-chv] [-t prefix] [-s secconds]\n"
	    "\th         - show this help\n"
	    "\tt prefix  - check mount points only with prefix\n"
	    "\t            example: -t \"/tpool\"\n"
	    "\ts seconds - wait secoonds (emulate mount)\n"
	    "\tc         - continue, if check is fail\n"
	    "\tv         - print process\n", prog);
}

int
main(int argc, char **argv)
{
	libzfs_handle_t *g_zfs;
	int c;

	(void) setlocale(LC_ALL, "");
	(void) setlocale(LC_NUMERIC, "C");
	(void) textdomain(TEXT_DOMAIN);

	options_t opts = {
	    .op_verbose = B_FALSE,
	    .op_continue = B_FALSE,
	    .op_seconds = 0,
	    .op_result = B_TRUE,
	    .op_prefix_path = NULL
	};

	while ((c = getopt(argc, argv, "t:s:chv")) != EOF) {
		switch (c) {
		case 't':
			opts.op_prefix_path = optarg;
			break;

		case 's':
			opts.op_seconds = atoi(optarg);
			break;

		case 'c':
			opts.op_continue = B_TRUE;
			break;

		case 'v':
			opts.op_verbose = B_TRUE;
			break;

		case 'h':
			usage(argv[0]);
			exit(EXIT_SUCCESS);

		default:
			usage(argv[0]);
			exit(EXIT_FAILURE);
		}
	}


	if ((g_zfs = libzfs_init()) == NULL) {
		fprintf(stderr, "%s\n", libzfs_error_init(errno));
		return (EXIT_FAILURE);
	}

	boolean_t res = do_mount_all(g_zfs, &opts);

	libzfs_fini(g_zfs);

	return (res ? EXIT_SUCCESS : EXIT_FAILURE);
}
